# Copyright 2019 The LUCI Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Defines luci.console_view(...) rule."""

load("@stdlib//internal/io.star", "io")
load("@stdlib//internal/lucicfg.star", "lucicfg")
load("@stdlib//internal/validate.star", "validate")
load("@stdlib//internal/luci/common.star", "keys", "kinds", "view")
load("@stdlib//internal/luci/proto.star", "milo_pb")
load("@stdlib//internal/luci/rules/console_view_entry.star", "console_view_entry")

# TODO(vadimsh): Document how builders should be configured to be eligible for
# inclusion into a console.

def _console_view(
        ctx,  # @unused
        *,
        name = None,
        title = None,
        repo = None,
        refs = None,
        exclude_ref = None,
        include_experimental_builds = None,
        header = None,
        favicon = None,
        default_commit_limit = None,
        default_expand = None,
        entries = None):
    r"""A Milo UI view that displays a table-like console.

    In this view columns are builders and rows are git commits on which builders
    are triggered.

    A console is associated with a single git repository it uses as a source of
    commits to display as rows. The watched ref set is defined via `refs` and
    optional `exclude_ref` fields. If `refs` are empty, the console defaults to
    watching `refs/heads/main`.

    `exclude_ref` is useful when watching for commits that landed specifically
    on a branch. For example, the config below allows to track commits from all
    release branches, but ignore the commits from the main branch, from which
    these release branches are branched off:

        luci.console_view(
            ...
            refs = ['refs/branch-heads/\d+\.\d+'],
            exclude_ref = 'refs/heads/main',
            ...
        )

    For best results, ensure commits on each watched ref have **committer**
    timestamps monotonically non-decreasing. Gerrit will take care of this if
    you require each commit to go through Gerrit by prohibiting "git push" on
    these refs.

    #### Adding builders

    Builders that belong to the console can be specified either right here:

        luci.console_view(
            name = 'CI builders',
            ...
            entries = [
                luci.console_view_entry(
                    builder = 'Windows Builder',
                    short_name = 'win',
                    category = 'ci',
                ),
                # Can also pass a dict, this is equivalent to passing
                # luci.console_view_entry(**dict).
                {
                    'builder': 'Linux Builder',
                    'short_name': 'lnx',
                    'category': 'ci',
                },
                ...
            ],
        )

    Or separately one by one via luci.console_view_entry(...) declarations:

        luci.console_view(name = 'CI builders')
        luci.console_view_entry(
            builder = 'Windows Builder',
            console_view = 'CI builders',
            short_name = 'win',
            category = 'ci',
        )

    Note that consoles support builders defined in other projects. See
    [Referring to builders in other projects](#external-builders) for more
    details.

    #### Console headers

    Consoles can have headers which are collections of links, oncall rotation
    information, and console summaries that are displayed at the top of a
    console, below the tree status information. Links and oncall information is
    always laid out to the left, while console groups are laid out to the right.
    Each oncall and links group take up a row.

    Header definitions are based on `Header` message in Milo's [project.proto].
    There are two way to supply this message via `header` field:

      * Pass an appropriately structured dict. Useful for defining small headers
        inline:

            luci.console_view(
                ...
                header = {
                    'links': [
                        {'name': '...', 'links': [...]},
                        ...
                    ],
                },
                ...
            )

      * Pass a string. It is treated as a path to a file with serialized
        `Header` message. Depending on its extension, it is loaded ether as
        JSONPB-encoded message (`*.json` and `*.jsonpb` paths), or as
        TextPB-encoded message (everything else):

            luci.console_view(
                ...
                header = '//consoles/main_header.textpb',
                ...
            )

    [project.proto]: https://chromium.googlesource.com/infra/luci/luci-go/+/refs/heads/main/milo/api/config/project.proto

    Args:
      ctx: the implicit rule context, see lucicfg.rule(...).
      name: a name of this console, will show up in URLs. Note that names of
        luci.console_view(...) and luci.list_view(...) are in the same namespace
        i.e. defining a console view with the same name as some list view (and
        vice versa) causes an error. Required.
      title: a title of this console, will show up in UI. Defaults to `name`.
      repo: URL of a git repository whose commits are displayed as rows in the
        console. Must start with `https://`. Required.
      refs: a list of regular expressions that define the set of refs to pull
        commits from when displaying the console, e.g. `refs/heads/[^/]+` or
        `refs/branch-heads/\d+\.\d+`. The regular expression should have a
        literal prefix with at least two slashes present, e.g.
        `refs/release-\d+/foobar` is *not allowed*, because the literal prefix
        `refs/release-` contains only one slash. The regexp should not start
        with `^` or end with `$` as they will be added automatically. If empty,
        defaults to `['refs/heads/main']`.
      exclude_ref: a single ref, commits from which are ignored even when they
        are reachable from refs specified via `refs` and `refs_regexps`. Note
        that force pushes to this ref are not supported. Milo uses caching
        assuming set of commits reachable from this ref may only grow, never
        lose some commits.
      header: either a string with a path to the file with the header definition
        (see io.read_file(...) for the acceptable path format), or a dict with
        the header definition.
      include_experimental_builds: if True, this console will not filter out
        builds marked as Experimental. By default consoles only show production
        builds.
      favicon: optional https URL to the favicon for this console, must be
        hosted on `storage.googleapis.com`. Defaults to `favicon` in
        luci.milo(...).
      default_commit_limit: if set, will change the default number of commits to
        display on a single page.
      default_expand: if set, will default the console page to expanded view.
      entries: a list of luci.console_view_entry(...) entities specifying
        builders to show on the console.
    """
    refs = validate.list("refs", refs) or ["refs/heads/main"]
    for r in refs:
        validate.string("refs", r)

    if header != None:
        if type(header) == "dict":
            header = milo_pb.Header(**header)
        elif type(header) == "string":
            header = io.read_proto(milo_pb.Header, header)
        else:
            fail('bad "header": got %s, want string or dict' % type(header))

    return view.add_view(
        key = keys.console_view(name),
        entry_kind = kinds.CONSOLE_VIEW_ENTRY,
        entry_ctor = console_view_entry,
        entries = entries,
        props = {
            "name": name,
            "title": validate.string("title", title, default = name, required = False),
            "repo": validate.repo_url("repo", repo),
            "refs": refs,
            "exclude_ref": validate.string("exclude_ref", exclude_ref, required = False),
            "header": header,
            "include_experimental_builds": validate.bool("include_experimental_builds", include_experimental_builds, required = False),
            "favicon": validate.string("favicon", favicon, regexp = r"https://storage\.googleapis\.com/.+", required = False),
            "default_commit_limit": validate.int("default_commit_limit", default_commit_limit, min = 0, max = 1000, default = 0, required = False),
            "default_expand": validate.bool("default_expand", default_expand, required = False),
        },
    )

console_view = lucicfg.rule(impl = _console_view)
